﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Xml.Linq;
using PSE.Framework.ErrorLogging;
using PSE.Updater.Common;
using PSE.Updater.Common.Gateway.DataContract;
using PSE.Updater.Common.Providers;
using PSE.Updater.Common.Providers.Interfaces;
using PSE.Updater.Server.Engine.Exceptions;

namespace PSE.Updater.Server.Engine.Providers.Updates
{
    /// <summary>
    /// Classe que representa os detalhes de um determinado arquivo
    /// </summary>
    internal class FileDetails : IFileDetails
    {
        private PackageType? _packageType = null;

        private ResourceExtractor _resourceExtractor;

        /// <summary>
        /// Construtor
        /// </summary>
        /// <param name="productName">Nome do Produto</param>
        public FileDetails(string productName)
        {
            this.SystemName = productName;
        }

        /// <summary>
        /// Inicializa as Informações do File
        /// </summary>
        /// <param name="path">Caminho do Arquivo</param>
        public void Initialize(string filePath, ProviderType providerType)
        {
            this.FilePath = filePath;

            if (string.IsNullOrEmpty(this.FilePath))
                throw new InvalidPackageException("O caminho do pacote está inválido", filePath);

            if (!File.Exists(this.FilePath))
                throw new InvalidPackageException("O arquivo informando não foi encontrado", filePath);

            this.ProviderType = ProviderType;

            LoadInformations(this.FilePath);
        }

        /// <summary>
        /// Carrega as informações sobre Arquivo
        /// </summary>
        /// <param name="filePath"></param>
        private void LoadInformations(string filePath)
        {
            AppDomain getResourceFileDomain = null;
            try
            {
                TryReadFile(filePath);

                getResourceFileDomain = AppDomain.CreateDomain("GetResourceFileDomain");
                //string path = Path.GetDirectoryName(System.Configuration.ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None).FilePath);

                string path = string.Empty;
                _resourceExtractor = (ResourceExtractor)getResourceFileDomain.CreateInstanceAndUnwrap("PSE.Updater.Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b4453a924d5fd421", "PSE.Updater.Common.ResourceExtractor");

                if (_resourceExtractor == null)
                    throw new Exception("Não foi possível recuperar uma instância da classe 'ResourceExtractor'");

                //recuperar o tamanho do arquivo
                this.Size = (new FileInfo(filePath)).Length;

                var resourceContent = GetResourceFile(filePath, "Resources.xml");

                var setupConfigPath = GetXmlSetupConfigurationFile(filePath, resourceContent, ref _packageType);
                if (!_packageType.HasValue)
                    throw new InvalidPackageException("Tipo do Pacote não identificado", filePath);
                //this.Type = type.Value;

                XDocument setupConfig = XDocument.Parse(setupConfigPath);
                if (setupConfig == null)
                    throw new InvalidPackageException("Não foi possível ler o arquivo com as informações da Atualização", filePath);

                XElement systemInfo = setupConfig.Descendants("SystemInfo").FirstOrDefault();
                if (systemInfo == null)
                    throw new InvalidPackageException("Informação sobre o produto não encontrada", filePath);

                this.SystemName = GetProductId(systemInfo);

                this.Version = GetVersion(systemInfo);

                this.ModuleName = GetModuleName(systemInfo);

                this.UpdateDescription = GetUpdateDescription(systemInfo);
            }
            catch (InvalidPackageException ex)
            {
                this.Status = PackageStatus.Invalid;
                throw ex;
            }
            catch (Exception ex)
            {
                this.Status = PackageStatus.Invalid;
                throw new InvalidPackageException("Não foi possível recuperar as informações do pacote.", filePath, ex);
            }
            finally
            {
                //liberar o domain, liberando assim o assembly
                if (getResourceFileDomain != null)
                {
                    AppDomain.Unload(getResourceFileDomain);
                }
            }
        }

        /// <summary>
        /// Antes de abrir, tenta ler o arquivo. 
        /// Isso é feito para saber se o arquivo está disponível para leitura
        /// </summary>
        /// <param name="filePath">Caminho do Arquivo</param>
        private static void TryReadFile(string filePath)
        {
            //codigo adicionado para resolver problema de concorrencia no acesso ao arquivo
            for (int i = 0; i < 60; i++)//esperar no maximo 5 minutos
            {
                FileStream fs = null;
                try
                {
                    //tentar abrir o arquivo para leitura
                    fs = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.None);
                    fs.Close();
                    fs.Dispose();
                    break;
                }
                catch (Exception ex)
                {
                    //logar a falhar ao tentar ler o arquivo
                    ErrorLogger.WriteLog(LogType.Error, ex);
                    //esperar 1 segundo
                    Thread.Sleep(5000);
                    continue;
                }
                finally
                {
                    if (fs != null)
                        fs.Dispose();
                }
            }
        }

        /// <summary>
        /// Retorna o nome do módulo
        /// </summary>
        /// <param name="xmlSystemInfo">Xml contendo a tag com o nome do módulo</param>
        /// <returns>Nome do Módulo</returns>
        private string GetProductId(XElement xmlSystemInfo)
        {
            return xmlSystemInfo.Attribute("Id").Value;
        }

        /// <summary>
        /// Retorna uma versão do Módulo
        /// </summary>
        /// <param name="xmlSystemInfo">Xml contendo a tag com a versão do módulo</param>
        /// <returns>Versão do Módulo</returns>
        private string GetVersion(XElement systemInfo)
        {
            XElement xmlVersion = systemInfo.Descendants("Version").FirstOrDefault();

            if (xmlVersion == null)
                throw new InvalidPackageException("Informação sobre a versão do produto não encontrada", this.FilePath);

            string major = GetVersionFeature(xmlVersion, "Major");
            string build = GetVersionFeature(xmlVersion, "Build");
            string Major = GetVersionFeature(xmlVersion, "Major");
            string minor = GetVersionFeature(xmlVersion, "Minor");
            string revision = GetVersionFeature(xmlVersion, "Revision");
            string minorRevision = GetVersionFeature(xmlVersion, "MinorRevision");
            string majorRevision = GetVersionFeature(xmlVersion, "MajorRevision");

            string version = string.Format("{0}.{1}.{2}.{3}", major, minor, revision, build);

            return version;
        }

        /// <summary>
        /// Obtém um determinado dígito  da versão do módulo
        /// </summary>
        /// <param name="element">Elemento que contém os dígitos</param>
        /// <param name="featureName">Nome do Dígito</param>
        /// <returns>Um determinado dígito  da versão do módulo</returns>
        private string GetVersionFeature(XElement element, string featureName)
        {
            XElement feature = element.Descendants(featureName).FirstOrDefault();
            if (feature == null)
                throw new InvalidPackageException(string.Format("Propriedade {0} da Versão não encontrada", featureName), this.FilePath);

            return feature.Value;
        }

        /// <summary>
        /// Obtém o nome do módulo
        /// </summary>
        /// <param name="xmlSystemInfo">Xml contendo a tag como o nome do módulo</param>
        /// <returns>O nome do módulo</returns>
        private string GetModuleName(XElement xmlSystemInfo)
        {
            IEnumerable<XElement> groups = xmlSystemInfo.Descendants("Groups");
            if (groups.Distinct().Count() == 0)
                throw new InvalidPackageException("Grupos não encontrados", this.FilePath);

            string modulesName = string.Empty;
            IEnumerable<XElement> modules = groups.FirstOrDefault().Descendants("Group").Descendants("Module");

            var modulesList = from m in modules
                              group m by GetAttributeValue(m.Attribute("Id")) into g
                              select g.Key;

            modulesList = modulesList.Distinct();
            if (modulesList.Count() == 0)
                throw new InvalidPackageException("Nome do Módulo não encontrado", this.FilePath);

            modulesList = modulesList.Where(m => !string.IsNullOrEmpty(m));

            /*Desconsidera INFRA*/
            //modulesList = modulesList.Where(m => !m.ToUpper().Equals("INFRA", StringComparison.CurrentCultureIgnoreCase));

            if (modulesList.Count() > 1)
            {
                foreach (var module in modulesList)
                {
                    modulesName += module + ";";
                }
                throw new InvalidPackageException(string.Format("Mais de um módulo encontrado para o {1}\n Módulos: {0}", this._packageType, modulesName), this.FilePath);
                //currentModule = modulesName;
            }

            string currentModule = modulesList.FirstOrDefault();
            if (string.IsNullOrEmpty(currentModule))
                throw new InvalidPackageException("Nome de módulo não encontrado para o arquivo", this.FilePath);

            return currentModule;
        }

        private string GetAttributeValue(XAttribute attrib)
        {
            if (attrib == null)
                return string.Empty;

            return attrib.Value;
        }

        /// <summary>
        /// Obtem a descrição da Atualização
        /// </summary>
        /// <param name="xmlSystemInfo">Xml contendo a tag de descrição de Atualização</param>
        /// <returns>Descrição da Atualização</returns>
        private string GetUpdateDescription(XElement xmlSystemInfo)
        {
            IEnumerable<XElement> updateInfo = xmlSystemInfo.Descendants("UpdateInfo");
            if (updateInfo.Count() == 0)
                throw new InvalidPackageException("Informações sobre a atualização não encontradas", this.FilePath);

            IEnumerable<XElement> descriptions = updateInfo.Descendants("Descriptions");
            if (descriptions.Count() == 0)
                throw new InvalidPackageException("Descrições sobre a atualização não encontradas", this.FilePath);

            IEnumerable<XElement> description = descriptions.Descendants("Description");
            StringBuilder sb = new StringBuilder();
            foreach (XElement item in description)
                sb.AppendLine(item.Value);

            string updateDescription = sb.ToString();

            //if (string.IsNullOrEmpty(updateDescription))
            //    throw new InvalidPackageException("Descrições sobre a atualização não encontradas", this.FilePath);

            return updateDescription;
        }

        /// <summary>
        /// Obtem o conteudo do arquivo de configuração que se encontra no resource
        /// </summary>
        /// <param name="filePath">Caminho do resource</param>
        /// <param name="xmlResourceContent">Conteudo do arquivo de resource</param>
        /// <param name="packageType">Tipo do Pacote</param>
        /// <returns>Conteudo do Arquivo de Configuração</returns>
        private string GetXmlSetupConfigurationFile(string filePath, string xmlResourceContent, ref PackageType? packageType)
        {
            XDocument document = XDocument.Parse(xmlResourceContent);
            XElement resource = document.Descendants("Resources").FirstOrDefault();

            string type = resource.Attribute("Type").Value;
            if (!string.IsNullOrEmpty(type))
            {
                packageType = new PackageType();
                switch (type.ToLower())
                {
                    case "hotfix": packageType = PackageType.Hotfix;
                        break;
                    case "servicepack": packageType = PackageType.ServicePack;
                        break;
                    case "rtm": packageType = PackageType.RTM;
                        break;
                    default: packageType = null;
                        break;
                }
            }

            IEnumerable<XElement> resources = document.Elements("Resources").Descendants();
            var name = (from extractPath in resources
                        where extractPath.Attribute("extractPath") != null &&
                              extractPath.Attribute("extractPath").Value.EndsWith("SetupConfiguration.xml")
                        select extractPath.Attribute("name").Value);

            if (name.Count() == 0)
                throw new InvalidPackageException("Informações sobre o resource não encontradas", this.FilePath);

            string content = GetResourceFile(filePath, name.FirstOrDefault());

            return content;
        }

        /// <summary>
        /// Obtem o conteudo de um arquivo no resource
        /// </summary>
        /// <param name="filePath">Caminho do resource</param>
        /// <param name="resourceFileName">Identificação do resource</param>
        /// <returns>Conteudo do Arquivo</returns>
        private string GetResourceFile(string filePath, string resourceFileName)
        {
            try
            {
                return _resourceExtractor.GetResourceFile(filePath, resourceFileName);
            }
            catch (Exception ex)
            {
                throw new InvalidPackageException(ex.Message, filePath);
            }
        }

        /// <summary>
        /// Caminho do Arquivo
        /// </summary>
        public string FilePath { get; set; }
        /// <summary>
        /// Tamanho do Arquivo
        /// </summary>
        public long Size { get; set; }
        /// <summary>
        /// Nome do Produto
        /// </summary>
        public string SystemName { get; set; }
        /// <summary>
        /// Nome do Módulo do Produto
        /// </summary>
        public string ModuleName { get; set; }
        /// <summary>
        /// Versão do Sistema
        /// </summary>
        public string Version { get; set; }
        /// <summary>
        /// Status do Arquivo
        /// </summary>
        public PackageStatus Status { get; set; }
        /// <summary>
        /// Descrição da Atualização
        /// </summary>
        public string UpdateDescription { get; set; }
        /// <summary>
        /// Tipo do Provider
        /// </summary>
        public ProviderType ProviderType { get; private set; }
        /// <summary>
        /// Tipo do Pacote
        /// </summary>
        public PackageType? FilePackageType { get { return this._packageType; } }
        /// <summary>
        /// Clona o Objeto
        /// </summary>
        /// <returns>O Objeto clonado</returns>
        public IFileDetails Clone()
        {
            return this.MemberwiseClone() as IFileDetails;
        }
    }
}
